--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit bf94ceebbb9cc0fc9bdf27d8a59bba832713b505
Parents : 3794e58
Author : Sudo-Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-01-01T20:17:56-06:00
feat(call): implement elapsed time display for active calls and improve UI responsiveness with updated button styles
Changes
2 files changed, 67 insertions(+), 15 deletions(-)
Diff
diff --git a/meshchatx/src/frontend/components/call/CallOverlay.vue b/meshchatx/src/frontend/components/call/CallOverlay.vue
index 3bb33122..fcb1f9d6 100644
--- a/meshchatx/src/frontend/components/call/CallOverlay.vue
+++ b/meshchatx/src/frontend/components/call/CallOverlay.vue
@@ -1,8 +1,8 @@
<template>
<div
v-if="activeCall"
- class="fixed bottom-4 right-4 z-[100] w-72 bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transition-all duration-300"
- :class="{ 'ring-2 ring-red-500 ring-opacity-50': isEnded }"
+ class="fixed bottom-4 right-4 z-[100] w-80 bg-white dark:bg-zinc-900 rounded-2xl shadow-2xl border border-gray-200 dark:border-zinc-800 overflow-hidden transition-all duration-300"
+ :class="{ 'ring-2 ring-red-500 ring-opacity-50': isEnded || wasDeclined }"
>
<!-- Header -->
<div class="p-3 flex items-center bg-gray-50 dark:bg-zinc-800/50 border-b border-gray-100 dark:border-zinc-800">
@@ -105,6 +105,12 @@
<span v-else-if="activeCall.status === 6">{{ $t("call.connected") }}</span>
<span v-else>{{ $t("call.status") }}: {{ activeCall.status }}</span>
</div>
+ <div
+ v-if="activeCall.status === 6 && !isEnded && elapsedTime"
+ class="text-xs text-gray-500 dark:text-zinc-400 mt-1 font-mono"
+ >
+ {{ elapsedTime }}
+ </div>
</div>
<!-- Stats (only when connected and not minimized) -->
@@ -123,11 +129,11 @@
</div>
<!-- Controls -->
- <div v-if="!isEnded" class="flex justify-center space-x-3">
+ <div v-if="!isEnded && !wasDeclined" class="flex flex-wrap justify-center gap-3">
<!-- Mute Mic -->
<button
type="button"
- :title="isMicMuted ? 'Unmute Mic' : 'Mute Mic'"
+ :title="isMicMuted ? $t('call.unmute_mic') : $t('call.mute_mic')"
class="p-3 rounded-full transition-all duration-200"
:class="
isMicMuted
@@ -142,7 +148,7 @@
<!-- Mute Speaker -->
<button
type="button"
- :title="isSpeakerMuted ? 'Unmute Speaker' : 'Mute Speaker'"
+ :title="isSpeakerMuted ? $t('call.unmute_speaker') : $t('call.mute_speaker')"
class="p-3 rounded-full transition-all duration-200"
:class="
isSpeakerMuted
@@ -206,9 +212,17 @@
class="size-5 shrink-0"
/>
<MaterialDesignIcon v-else icon-name="account" class="size-5 text-blue-500 shrink-0" />
- <span class="text-sm font-medium text-gray-700 dark:text-zinc-200 truncate block">
- {{ activeCall.remote_identity_name || $t("call.unknown") }}
- </span>
+ <div class="flex flex-col min-w-0">
+ <span class="text-sm font-medium text-gray-700 dark:text-zinc-200 truncate block">
+ {{ activeCall.remote_identity_name || $t("call.unknown") }}
+ </span>
+ <span
+ v-if="activeCall.status === 6 && elapsedTime"
+ class="text-[10px] text-gray-500 dark:text-zinc-400 font-mono"
+ >
+ {{ elapsedTime }}
+ </span>
+ </div>
</div>
<div class="flex items-center space-x-1">
<button
@@ -260,6 +274,7 @@ export default {
data() {
return {
isMinimized: false,
+ elapsedTimeInterval: null,
};
},
computed: {
@@ -269,6 +284,23 @@ export default {
isSpeakerMuted() {
return this.activeCall?.is_speaker_muted ?? false;
},
+ elapsedTime() {
+ if (!this.activeCall?.call_start_time) {
+ return null;
+ }
+ const elapsed = Math.floor(Date.now() / 1000 - this.activeCall.call_start_time);
+ return Utils.formatMinutesSeconds(elapsed);
+ },
+ },
+ mounted() {
+ this.elapsedTimeInterval = setInterval(() => {
+ this.$forceUpdate();
+ }, 1000);
+ },
+ beforeUnmount() {
+ if (this.elapsedTimeInterval) {
+ clearInterval(this.elapsedTimeInterval);
+ }
},
methods: {
formatDestinationHash(hash) {
diff --git a/meshchatx/src/frontend/components/call/CallPage.vue b/meshchatx/src/frontend/components/call/CallPage.vue
index 1654b05d..2c8343d7 100644
--- a/meshchatx/src/frontend/components/call/CallPage.vue
+++ b/meshchatx/src/frontend/components/call/CallPage.vue
@@ -135,6 +135,12 @@
</span>
</template>
</div>
+ <div
+ v-if="activeCall && activeCall.status === 6 && !isCallEnded && elapsedTime"
+ class="text-gray-500 dark:text-zinc-400 mb-4 text-center font-mono text-lg"
+ >
+ {{ elapsedTime }}
+ </div>
<!-- settings during connected call -->
<div v-if="activeCall && activeCall.status === 6" class="mb-4">
@@ -209,16 +215,16 @@
</div>
<!-- actions -->
- <div v-if="activeCall" class="mx-auto space-x-4">
+ <div v-if="activeCall" class="flex flex-wrap justify-center gap-4 mt-6">
<!-- answer call -->
<button
v-if="activeCall.is_incoming && activeCall.status === 4"
:title="$t('call.answer_call')"
type="button"
- class="inline-flex items-center gap-x-2 rounded-2xl bg-green-600 px-6 py-4 text-lg font-bold text-white shadow-xl hover:bg-green-500 transition-all duration-200 animate-bounce"
+ class="inline-flex items-center gap-x-2 rounded-2xl bg-green-600 px-5 py-3 text-base font-bold text-white shadow-xl hover:bg-green-500 transition-all duration-200 animate-bounce"
@click="answerCall"
>
- <MaterialDesignIcon icon-name="phone" class="size-6" />
+ <MaterialDesignIcon icon-name="phone" class="size-5" />
<span>{{ $t("call.accept") }}</span>
</button>
@@ -227,10 +233,10 @@
v-if="activeCall.is_incoming && activeCall.status === 4"
:title="$t('call.send_to_voicemail')"
type="button"
- class="inline-flex items-center gap-x-2 rounded-2xl bg-blue-600 px-6 py-4 text-lg font-bold text-white shadow-xl hover:bg-blue-500 transition-all duration-200"
+ class="inline-flex items-center gap-x-2 rounded-2xl bg-blue-600 px-5 py-3 text-base font-bold text-white shadow-xl hover:bg-blue-500 transition-all duration-200"
@click="sendToVoicemail"
>
- <MaterialDesignIcon icon-name="voicemail" class="size-6" />
+ <MaterialDesignIcon icon-name="voicemail" class="size-5" />
<span>{{ $t("call.send_to_voicemail") }}</span>
</button>
@@ -242,10 +248,10 @@
: $t('call.hangup_call')
"
type="button"
- class="inline-flex items-center gap-x-2 rounded-2xl bg-red-600 px-6 py-4 text-lg font-bold text-white shadow-xl hover:bg-red-500 transition-all duration-200"
+ class="inline-flex items-center gap-x-2 rounded-2xl bg-red-600 px-5 py-3 text-base font-bold text-white shadow-xl hover:bg-red-500 transition-all duration-200"
@click="hangupCall"
>
- <MaterialDesignIcon icon-name="phone-hangup" class="size-6 rotate-[135deg]" />
+ <MaterialDesignIcon icon-name="phone-hangup" class="size-5 rotate-[135deg]" />
<span>{{
activeCall.is_incoming && activeCall.status === 4
? $t("call.decline")
@@ -926,6 +932,7 @@ export default {
ringtones: [],
editingRingtoneId: null,
editingRingtoneName: "",
+ elapsedTimeInterval: null,
};
},
computed: {
@@ -935,6 +942,13 @@ export default {
isSpeakerMuted() {
return this.activeCall?.is_speaker_muted ?? false;
},
+ elapsedTime() {
+ if (!this.activeCall?.call_start_time) {
+ return null;
+ }
+ const elapsed = Math.floor(Date.now() / 1000 - this.activeCall.call_start_time);
+ return Utils.formatMinutesSeconds(elapsed);
+ },
},
mounted() {
this.getConfig();
@@ -959,6 +973,11 @@ export default {
this.getVoicemails();
}, 10000);
+ // update elapsed time every second
+ this.elapsedTimeInterval = setInterval(() => {
+ this.$forceUpdate();
+ }, 1000);
+
// autofill destination hash from query string
const urlParams = new URLSearchParams(window.location.search);
const destinationHash = urlParams.get("destination_hash");
@@ -969,6 +988,7 @@ export default {
beforeUnmount() {
if (this.statusInterval) clearInterval(this.statusInterval);
if (this.historyInterval) clearInterval(this.historyInterval);
+ if (this.elapsedTimeInterval) clearInterval(this.elapsedTimeInterval);
if (this.endedTimeout) clearTimeout(this.endedTimeout);
if (this.audioPlayer) {
this.audioPlayer.pause();
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────